﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace ebc.minibus
{
    public class EbcMiniBus
    {
        public class Registration : IDisposable
        {
            private readonly Action unregister;


            public Registration(Action unregister)
            {
                this.unregister = unregister;
            }


            public void Dispose()
            {
                unregister();
            }
        }


        private readonly Dictionary<Type, List<KeyValuePair<string, Action<object>>>> handlers;
        private readonly ReaderWriterLock handlersLock = new ReaderWriterLock();


        public EbcMiniBus()
        {
            Trace.TraceInformation("EbcMiniBus started");
            
            this.handlers = new Dictionary<Type, List<KeyValuePair<string, Action<object>>>>();
        }

        
        public IDisposable Register<TInput>(Action<TInput> inputHandler, string name="")
        {
            Trace.TraceInformation("EbcMiniBus.Register handler '{0}' for type {1}", name, typeof(TInput).Name);

            this.handlersLock.AcquireWriterLock(500);
            try
            {
                List<KeyValuePair<string, Action<object>>> handlersForType;
                if (!this.handlers.TryGetValue(typeof (TInput), out handlersForType))
                    this.handlers.Add(typeof (TInput),
                                      handlersForType = new List<KeyValuePair<string, Action<object>>>());

                var registrant = new KeyValuePair<string, Action<object>>(name, msg => inputHandler((TInput) msg));
                handlersForType.Add(registrant);

                return new Registration(() => Unregister(registrant));
            }
            finally
            {
                this.handlersLock.ReleaseWriterLock();
            }
        }

        public IDisposable RegisterOnCurrentSyncContext<TInput>(Action<TInput> inputHandler, string name = "")
        {
            return Register(new SyncCtxSwitch().CreateHandler(inputHandler), name);
        }


        public IDisposable Register<TInput, TOutput>(Func<TInput, TOutput> inputHandler, string inputName = "", params string[] outputNames)
        {
            return Register<TInput>(msg => this.Send(inputHandler(msg), outputNames), inputName);
        }

        public IDisposable RegisterOnCurrentSyncContext<TInput, TOutput>(Func<TInput, TOutput> inputHandler, string inputName = "", params string[] outputNames)
        {
            return Register(new SyncCtxSwitch().CreateHandler(inputHandler), inputName, outputNames);
        }
       

        public void Send<T>(T message) { this.Send(message, new string[]{});}
        public void Send<T>(T message, params string[] names)
        {
            if (names == null || names.Length == 0) names = new[] {""};

            Trace.TraceInformation("EbcMiniBus.Send({0},[{1}]) on thread #{2}", message.GetType(), string.Join(",", names), Thread.CurrentThread.GetHashCode());

            Collect_subscribers_for_named_messagetype<T>(names)
                .ForEach(handler => handler(message));
        }

        private List<Action<object>> Collect_subscribers_for_named_messagetype<T>(IEnumerable<string> names)
        {
            var relevantHandlers = new List<Action<object>>();
            this.handlersLock.AcquireReaderLock(500);
            try
            {
                relevantHandlers.AddRange(from name in names
                                          from handler in this.handlers[typeof (T)].Where(h => h.Key == name)
                                          select handler.Value);
            }
            finally
            {
                this.handlersLock.ReleaseReaderLock();
            }
            return relevantHandlers;
        }


        public void Post<T>(T message) { this.Post(message, new string[] { }); }
        public void Post<T>(T message, params string[] names)
        {
            new Task(() => {
                                try
                                {
                                    this.Send(message, names);
                                }
                                catch (Exception ex)
                                {
                                    Trace.TraceWarning("EbcMiniBus.Post caught exception {0} on thread #{1}", ex, Thread.CurrentThread.GetHashCode());
                                    this.OnBackgroundException(ex);
                                }
                           }).Start();
        }


        public event Action<Exception> OnBackgroundException;


        private void Unregister(object registrant)
        {
            this.handlersLock.AcquireWriterLock(500);
            try
            {
                foreach (var typeHandler in this.handlers.Where(typeHandler => typeHandler.Value.Any(namedHandler => namedHandler.Equals(registrant))))
                {
                    Trace.TraceInformation("EbcMiniBus.Unregister handler '{0}' for type {1}", ((KeyValuePair<string, Action<object>>) registrant).Key, typeHandler.Key.Name);
                    typeHandler.Value.Remove((KeyValuePair<string, Action<object>>) registrant);
                    return;
                }
            }
            finally
            {
                this.handlersLock.ReleaseWriterLock();
            }
        }
    }
}
